Skip to content

🐛 Move __tablename__ default from @declared_attr to metaclass#1821

Open
estarfoo wants to merge 1 commit intofastapi:mainfrom
estarfoo:feat/tablename-property
Open

🐛 Move __tablename__ default from @declared_attr to metaclass#1821
estarfoo wants to merge 1 commit intofastapi:mainfrom
estarfoo:feat/tablename-property

Conversation

@estarfoo
Copy link
Copy Markdown

Move the default __tablename__ from a @declared_attr method to SQLModelMetaclass.__new__, where it is set in the class dict before class creation, alongside the existing setup.

This resolves the type-level contradiction between the ClassVar[str | Callable] declaration and the @declared_attr descriptor, letting __tablename__ = "my_table" work without # type: ignore for pyright.

Tests added for default name, explicit override, inheritance, and non-table models.

Fixes #98.

This is split out from #1820, dropping those changes which would be resolved by #1345 or #1806.

The `SQLModel` base class declared `__tablename__` both as a `ClassVar`
and as a `@declared_attr` method.  Some type checkers (pyright) see the
descriptor type from `@declared_attr`, so setting
`__tablename__ = "my_table"` in a subclass is rejected as a type
mismatch, even though it works at runtime.

Replace the `@declared_attr` method with a default set in
`SQLModelMetaclass.__new__` via `dict_used`, before class creation.

Fixes fastapi#98.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@svlandeg svlandeg added the bug Something isn't working label Mar 18, 2026
@YuriiMotov
Copy link
Copy Markdown
Member

Spent a little time on reviewing this PR and haven't found any issues so far. All seems to work well.

Would be also good to fix the case with dynamically created table names as well:

from pydantic.alias_generators import to_snake
from sqlalchemy.orm import declared_attr
from sqlmodel import Field, SQLModel


class FirstWidget(SQLModel):

    id: int | None = Field(default=None, primary_key=True)
    name: str

    @declared_attr.directive
    @classmethod
    def __tablename__(cls) -> str:
        return to_snake(cls.__name__)


assert FirstWidget.__tablename__ == "first_widget"

Works well, but pyright argues:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pyright cannot recognize the type of SQLModel.__tablename__

4 participants